package org.blankie.lang;

import java.beans.Introspector;
import java.beans.IntrospectionException;
import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import java.util.logging.Logger;
import java.util.logging.Level;

/**
 * This class provides utility static methods to evaluate method pre-conditions. It has a <em>mode</em>
 * defined in a static field that alters its behavior in case of error.
 *
 * <dl>
 * <dt>ValidationMode.BUG_AWARE</dt>
 * <dd>If a pre-condition isn't respected a runtime exception is thrown.</dd>
 * <dt>ValidationMode.QUALITY_AWARE</dt>
 * <dd>If a pre-conition isn't respected a {@link FailSafeException} is thrown with an appropriate
 * </dd>
 * <dt>ValidationMode.SPEED_AWARE</dt>
 * <dd>In this specific case, no checkings are done.</dd>
 * </dl>
 *
 * @author francois.mora@blankie.org
 */
public class Validate {

    /**
     * Returns the current validation mode.
     * @return A non <code>null</code> <code>ValidationMode</code>.
     */
    public static ValidationMode getMode() {
        return mode;
    }

    /**
     * Set the current validation mode.
     * @param mode The new validation mode. If <code>null</code> it is reset to
     * <code>ValidationMode.BUG_AWARE</code>.
     */
    public static void setMode(ValidationMode mode) {
        Validate.mode = (mode == null ? ValidationMode.BUG_AWARE : mode);
    }

    /**
     * This method performs a failing check with the given <var>message</var>.
     * @param message the failing message
     * @param args the message arguments.
     */
    public static void fail(final String message, String... args) {
        validate(new BaseChecker(message, args) {
            public int check() {
                return -1;
            }
        });
    }

    /**
     * This method checks the given <var>expression</var> is not <code>null</code>.
     * @param expression The expression to test.
     */
    public static void notNull(final Object expression) {
        validate(new BaseChecker("An expression is null") {
            public int check() {
                return (expression == null ? -1 : 1);
            }
        });
    }

    public static void equals(final Object expression, final Object value) {
        validate(new BaseChecker("An expression is incorrect") {
            public int check() {
                if (expression == null) {
                    return value == null ? 1 : -1;
                } else {
                    return expression.equals(value) ? 1 : -1;
                }
            }
        });
    }

    /**
     * This method checks an expression is not null.
     * @param expression The expression to evaluate. It must not be <code>null</code> and it must neither
     * start nor end with the dot character.
     * @param parameter The parameter to check.
     */
    public static void notNull(final String expression, final Object parameter) {
        validate(new BaseChecker("The expression %s is null", expression) {
            public int check() {
                Object result = evaluate(expression, parameter);

                if (result == null) {
                    return -1;
                } else if (result == EVALUATION_ERROR) {
                    return 0;
                } else {
                    return 1;
                }
            }
        });
    }

    public static void parameterNonEmptyElements(final String parameterName, String[] elements) {
        parameterNotNull(parameterName, (Object[]) elements);
        validate(new LoopChecker("An element is empty", elements) {
            @Override
            public int doCheck(Object object) {
                return ((object == null) || ((String) object).length() == 0) ? -1 : 1;
            }
        });
    }

    /**
     * This method checks an expression is equal to a given value using the <code>equals()</code>
     * method of the given <var>object</var> parameter.
     * @param expression The expression to evaluate. It must not be <code>null</code> and it must neither
     * start nor end with the dot character.
     * @param parameter The parameter to check.
     * @param object The expected parameter value.
     */
    public static void equals(final String expression, final Object parameter, final Object object) {
        validate(new BaseChecker("The expression %s is incorrect", expression) {
            public int check() {
                Object result = evaluate(expression, parameter);

                if (object == null) {
                    return result == null ? 1 : -1;
                } else if (object == EVALUATION_ERROR) {
                    return 0;
                } else {
                    return object.equals(result) ? 1 : -1;
                }
            }
        });
    }

    /**
     * This method checks the parameter <var>objects</var> aren't <code>null</code>. The parameter
     * names are given as a comma separated list of names stored in the <var>parameterNames</var> parameter.
     * @param parameterNames The parameter names. A non <code>null</code> list of comma separated names.
     * @param objects The parameter to check. The number of objects must be the same of the number of names.
     */
    public static void parameterNotNull(String parameterNames, final Object... objects) {
        validate(new LoopChecker("The '%s' parameter must not be null", parameterNames, objects) {
            @Override
            public int doCheck(Object object) {
                return object != null ? 1 : -1;
            }
        });
    }

    /**
     * This method checks the given <var>object</var> is an instance of all the given <var>classes</var>m
     * that's to say it implements all the given interfaces.
     * @param parameterName The name of the checked parameter. It must not be blank.
     * @param object The parameter value.
     * @param classes The list of classes the object has to extend or implement. There must not be any 
     * <code>null</code> elements.
     */
    public static void parameterNotNullInstanceOf(String parameterName, final Object object,
            final Class<?>... classes) {
        assert Assert.notBlank(parameterName);

        // Concatenates the classes names for the message.
        StringBuilder builder = new StringBuilder("The '%s' parameter must be a non null instance of ");

        for (Class<?> clazz : classes) {
            builder.append(", ").append(clazz.getName());
        }

        validate(new BaseChecker(builder.append(".").substring(2), parameterName) {
            public int check() {
                if (object == null) {
                    return -1;
                }

                for (Class<?> clazz : classes) {
                    if (!clazz.isAssignableFrom(object.getClass())) {
                        return -1;
                    }
                }

                return 1;
            }
        });
    }

    /**
     * This interface is implemented by the codes that perform a check and provide a message when that check
     * has failed.
     */
    private interface Checker {

        /**
         * Perform a check.
         * @return <code>1</code> if the check has succedeed, <code>-1</code>
         * if it failed or <code>0</code> if couldn't be performed.
         */
        int check();
        
        /**
         * This method returns the message that follows the failed check.
         * @return A non <code>null</code> <code>String</code>.
         */
        String getMessage();

    }

    /**
     * This class is an abstract implementation of <code>Checker</code> that uses a formatted (as in
     * {@link String#format}) message.
     */
    private static abstract class BaseChecker implements Checker {

        /**
         * The checker formatted message.
         */
        private final String message;

        /**
         * Creates a new checker with a message obtained with the method call :
         * <code>String.format(format, (Object[]) args)</code>.
         * @param format The checker message format. It must not be <code>null</code>.
         * @param args The format arguments.
         */
        BaseChecker(String format, String... args) {
            assert format != null;
            this.message = String.format(format, (Object[]) args);
        }

        /**
         * This method returns the checker message.
         * @return A non <code>null</code> message
         */
        @Override
        public String getMessage() {
            return message;
        }

    }
    
    /**
     * This class is an abstact Checker implementation that performs the same check on every elements of
     * a given array of objects. The checker message is a formatted message which values are defined per
     * array elements in the class constructor.
     */
    private static abstract class LoopChecker implements Checker {
        
        /**
         * The checker message format.
         */
        private final String format;
        
        /**
         * The objects to check.
         */
        private final Object[] objects;
        
        /**
         * The checker message arguments for every checked object. This an array of space separated arguments.
         */
        private final String[] ssvArgs;
        
        /**
         * The checker message.
         */
        private String message;
        
        /**
         * Creates a loop checker for the given <var>objects</var>. The message arguments are stored in
         * <var>csvArgs</var> as a comma separated list of space separated arguments.
         * @param format A String format. It must not be <code>null</code>.
         * @param csvArgs A non blank comma separated list of space separated format arguments. There must be
         * as many comma separated values than <var>objects</var> has elements.
         * @param objects the objects to check. It must not be <code>null</code>.
         */
        LoopChecker(String format, String csvArgs, Object[] objects) {
            assert format != null && Assert.notBlank(csvArgs) && objects != null;
            this.format = format;
            this.ssvArgs = csvArgs.split(",\\s*");
            assert this.ssvArgs.length == objects.length;
            this.objects = objects;
        }
        
        LoopChecker(String format, Object[] objects) {
            assert format != null && objects != null;
            this.format = format;
            this.objects = objects;
            
            this.ssvArgs = new String[objects.length];
            for (int i = 0; i < objects.length; i++) {
                objects[i] = String.valueOf(i);
            }
        }
        
        /**
         * This method iterates over the object array and delegate the checking to the <code>doCheck()</code>
         * method.
         * @return true if the checkings succeeded.
         */
        @Override
        public int check() {
            int result = 1;

            for (int i = 0; i < objects.length; i++) {
                int check = doCheck(objects[i]);

                if (check == -1) {
                    message = String.format(format, (Object[]) ssvArgs[i].split("\\s+"));
                    return -1;
                } else if (check == 0) {
                    result = 0;
                }
            }

            return result;
        }

        /**
         * The last failed checking message.
         */
        @Override
        public String getMessage() {
            return message;
        }
        
        /**
         * This method performs the checkings on a array element.
         * @return true if the checking succeeded.
         */
        protected abstract int doCheck(Object object);

    }

    /**
     * The class logger.
     */
    private static final Logger logger = Logger.getLogger(Validate.class.getName());

    /**
     * The object returned by the <code>evaluate()</code> method when the evaluation failed.
     */
    private static final Object EVALUATION_ERROR = new Object();

    /**
     * The class validation mode.
     */
    private static ValidationMode mode = ValidationMode.BUG_AWARE; 

    /**
     * This method validate a check by calling the <var>checker</var>. The method has the following
     * behaviour according to the current validation mode.
     * <dl>
     * <dt>BUG_AWARE</dt>
     * <dd>If the checking fails it throws an <code>IllegalArgumentException</code> exception.</dd>
     * <dt>SPEED_AWARE</dt>
     * <dd>The checks are skipped.</dd>
     * <dt>QUALITY_AWARE</dt>
     * <dd>If the checking fails it throws a <code>FailSafeException</code> exception.</dd>
     * </dl>
     * @param checker The checker to use. It must no be <code>null</code>.
     */
    private static void validate(Checker checker) {
        assert checker != null;
        
        switch (mode) {
        case BUG_AWARE:
            int check = checker.check();

            if (check == -1) {
                throw new IllegalArgumentException(checker.getMessage());
            } else if (check == 0) {
                throw new IllegalStateException("A validation checking couldn't be performed");
            }
            break;
        case SPEED_AWARE:
            break;
        case QUALITY_AWARE:
            if (checker.check() == -1) {
                throw new FailSafeException(checker.getMessage());
            }
            break;
        default:
            assert false;
        }
    }

    /**
     * This method returns the first name of the given <var>expression</var>, that's to say it returns the
     * first characters ended by a dot.
     * @param expression A non <code>null</code> string.
     * @return a non <code>null</code> string.
     */
    private static String getFirstName(String expression) {
        assert expression != null;

        int pos = expression.indexOf(".");
        assert pos > 0 && pos < expression.length() - 1;

        if (pos == -1) {
            return expression;
        } else {
            return expression.substring(0, pos);
        }
    }

    /** 
     * This method evaluates a pseudo bean path expression consisting of <em>Java</em> identifiers
     * concatened with the dot character, where the first identifier is the <var>object</var> name
     * and the following names are property names. <samp>"bean.parent.name"</samp> is evaluated
     * as the <var>name</var> of the bean referenced by the <var>parent</var> property of the
     * <var>bean</var> object.
     * <p>
     * If during the evaluation process the method evaluates a <code>null</code> then it returns
     * <code>null</code>. If an error occurs when accessing the <var>object</var> using reflection
     * then it returns {@link #EVALUATION_ERROR}.
     *
     * @param expression the expression to evaluate. It must not be <code>null</code>. It must not
     *                   start nor end with the dot character.
     * @param object     the object used for the expression evaluation.
     */
    private static Object evaluate(String expression, Object object) {
        assert expression != null;

        // Search for the first identifier.
        int pos = expression.indexOf(".");
        assert (pos > 0) && (pos < expression.length() - 1);

        // If the expression doesn't contain any dot character, then it is merely the object name.
        // So return the object.
        if (pos == -1) {
            return object;
        }

        // Start the evaluation loop.
        String[] names = expression.substring(pos + 1).split("\\.");
        Object result = object;
        for (int i = 0; (result != null) && (result != EVALUATION_ERROR) && (i < names.length); i++) {
            try {
                BeanInfo beanInfo = Introspector.getBeanInfo(result.getClass());
                Method getterMethod = null;

                for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
                    if (names[i].equals(descriptor.getName())) {
                        getterMethod = descriptor.getReadMethod();
                        break;
                    }
                }
                
                if (getterMethod == null) {
                    logger.log(Level.WARNING,
                               String.format("Error when validating the expression %s on the object " +
                                             "%s : The property %s isn't readable",
                                             expression, String.valueOf(object), names[i]));
                    result = EVALUATION_ERROR;
                } else {
                    result = getterMethod.invoke(result);
                }
            } catch (InvocationTargetException e) {
                logger.log(Level.WARNING,
                           String.format("Error when validating the expression %s on the object %s",
                                         expression, String.valueOf(object)), e.getCause());
                result = EVALUATION_ERROR;
            } catch (IllegalAccessException e) {
                logger.log(Level.WARNING,
                           String.format("Error when validating the expression %s on the object %s",
                                         expression, String.valueOf(object)), e);
                result = EVALUATION_ERROR;
            } catch (IntrospectionException e) {
                logger.log(Level.WARNING,
                           String.format("Error when validating the expression %s on the object %s",
                                         expression, String.valueOf(object)), e);
                result = EVALUATION_ERROR;
            }
        }

        return result;
    }

}
